V8 引擎的 BUG

console.log("script start");

async function async1() {
  // 如果 await 函数后面的函数是普通函数,那么其后的微任务就正常执行;否则,会将其再放入微任务队列。
  await async2();
  console.log("async1 end");
}

async function async2() {
  // 获取也可以认为这里是异步函数的构造函数,会同步执行的。这么理解也 ok
  console.log("async2 end");
}
async1();

setTimeout(function () {
  console.log("setTimeout");
}, 0);

new Promise((resolve) => {
  console.log("Promise");
  resolve();
})
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });

console.log("script end");
// 在`Chrome 66`和`node v10`中,正确输出是
// script start
// async2 end
// Promise
// script end
// promise1
// promise2
// async1 end
// setTimeout

// 注意:在新版本的浏览器中,`await`输出顺序被“提前”了。
// script start
// async2 end
// Promise
// script end
// async1 end
// promise1
// promise2
// setTimeout

当我们调用 async1 函数时,会马上输出 async2 end,并且函数返回一个 Promise,接下来在遇到 await的时候会就让出线程开始执行 async1 外的代码,所以我们完全可以把 await 看成是让出线程的标志。

async function async1() {
  await async2();
  console.log("async1 end");
}
// 约等于下面的实现
function async1() {
  return new Promise((resolve, reject) => {
    console.log("async2 end");
    // Promise.resolve() 将代码插入微任务队列尾部
    // resolve 再次插入微任务队列尾部
    resolve(Promise.resolve());
  }).then(() => {
    console.log("async1 end");
  });
}

也就是说,如果 await 后面跟着 Promise 的话,async1 end 需要等待三个 tick 才能执行到。V8 团队借鉴了 Node 8 中的一个 Bug,在引擎底层将三次 tick 减少到了二次 tick。

再谈 async 和 await

细心的朋友肯定会发现前面第 6 步,如果async2函数是没有async关键词修饰的一个普通函数呢?

// 新的async2函数
function async2() {
  console.log("async2 end");
}

输出结果如下所示:

script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout

不同的结果就出现在前面所说的第 6 步:如果 await 函数后面的函数是普通函数,那么其后的微任务就正常执行;否则,会将其再放入微任务队列。

其实是 V8 引擎的 BUG

看到前面,正常人都会觉得真奇怪!(但是按照上面的诀窍倒也是可以理解)

然而 V8 团队确定了这是个 bug(很多强行解释要被打脸了),具体的 PR请看这里open in new window。好在,这个问题已经在最新的 Chrome 浏览器中被修复了

简单点说,前面两段不同代码的运行结果都是:

script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout

await就是让出线程,其后的代码放入微任务队列(不会再多一次放入的过程),就这么简单了。

异步笔试题

var p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error("p1 中failure")), 3000);
});

var p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000);
});
var p3 = new Promise(function (resolve, reject) {
  resolve(2);
});
var p4 = new Promise(function (resolve, reject) {
  reject(new Error("error  in  p4"));
});

p3.then((re) => console.log(re)); //?
p4.catch((error) => console.log(error)); //?

p2.then(null, (re) => console.log(re)); //?
p2.catch((re) => console.log(re)); //?
// 2, "error in p4 "这是立即打印出来的。
//  3S 后会打印出两个'p1 中 failure'
var p1 = Promise.resolve(1);
var p2 = new Promise((resolve) => {
  setTimeout(() => resolve(2), 100);
});
var v3 = 3;
var p4 = new Promise((resolve, reject) => {
  setTimeout(() => reject("oops"), 10);
});

var p5 = new Promise((resolve) => {
  setTimeout(() => resolve(5), 0);
});
Promise.race([v3, p1, p2, p4, p5]).then((val) => console.log(val)); //?
Promise.race([p1, v3, p2, p4, p5]).then((val) => console.log(val)); // ?
Promise.race([p1, p2, p4, p5]).then((val) => console.log(val)); // ?
Promise.race([p2, p4, p5]).then((val) => console.log(val)); //?

打印顺序是:3 1 1 5

function Promise1() {
  return new Promise(function (resolve, reject) {
    for (let i = 0; i < 2; i++) {
      console.log("111");
    }
    resolve(true);
  });
}
function Promise2() {
  return new Promise(function (resolve, reject) {
    for (let i = 0; i < 2; i++) {
      console.log("222");
    }
    resolve(true);
  });
}

setTimeout(function () {
  console.log("333");
}, 0); // 这是是会执行的。考察的是异步执行,js的任务队列

Promise.all([Promise1(), Promise2()]).then(function () {
  console.log("All Done!");
});
// '111'
// '111'
// '222'
// '222'
// 'All Done!'
// '333'
// 请写出输出内容
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});
console.log("script end");
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');


script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

变式一

在第一个变式中我将 async2 中的函数也变成了 Promise 函数,代码如下:

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  //async2做出如下更改:
  new Promise(function (resolve) {
    console.log("promise1");
    resolve();
  }).then(function () {
    console.log("promise2");
  });
}
console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);
async1();

new Promise(function (resolve) {
  console.log("promise3");
  resolve();
}).then(function () {
  console.log("promise4");
});

console.log("script end");

可以先自己看看输出顺序会是什么,下面来公布结果:

script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout

在第一次 macro-task 执行完之后,也就是输出script end之后,会去清理所有 micro-task。所以会相继输出promise2async1 endpromise4,其余不再多说。

变式二

在第二个变式中,我将 async1 中 await 后面的代码和 async2 的代码都改为异步的,代码如下:

async function async1() {
  console.log("async1 start");
  await async2();
  //更改如下:
  setTimeout(function () {
    console.log("setTimeout1");
  }, 0);
}
async function async2() {
  //更改如下:
  setTimeout(function () {
    console.log("setTimeout2");
  }, 0);
}
console.log("script start");

setTimeout(function () {
  console.log("setTimeout3");
}, 0);
async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});
console.log("script end");

可以先自己看看输出顺序会是什么,下面来公布结果:

script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1

在输出为promise2之后,接下来会按照加入 setTimeout 队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1,所以会按 3,2,1 的顺序来输出。

变式三

变式三是我在一篇面经中看到的原题,整体来说大同小异,代码如下:

async function a1() {
  console.log("a1 start");
  await a2();
  console.log("a1 end");
}
async function a2() {
  console.log("a2");
}

console.log("script start");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

// Promise.resolve() 返回一个已经执行完毕状态的 Promise
Promise.resolve().then(() => {
  console.log("promise1");
});

a1();

let promise2 = new Promise((resolve) => {
  resolve("promise2.then");
  // Promise 构造函数中除了 resolve 返回部分,其余全是同步执行
  console.log("promise2");
});

promise2.then((res) => {
  console.log(res);
  Promise.resolve().then(() => {
    console.log("promise3");
  });
});
console.log("script end");
// script start
// a1 start
// a2
// promise2
// script end
// promise1
// a1 end
// promise2.then
// promise3
// setTimeout
setTimeout(() => {
  console.log(1);
}, 0);

new Promise((resolve) => {
  // Promise 构造函数是同步执行
  console.log(2);
  resolve();
  console.log(3);
}).then(() => {
  console.log(4);
});

console.log(5);
// 2 3 5 4 1
Promise.resolve()
  .then(() => {
    console.log(0);
    // 相当于 Promise.resolve(Promise.resolve(4)) Promise.resolve 执行一次相当于将任务往微任务末尾插入一次
    return Promise.resolve(4);
  })
  .then((res) => console.log(res || "xxxx"));

Promise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(5);
  })
  .then(() => {
    console.log(6);
  });
// 0, 1, 2, 3, 4, 5, 6
// 状态只会改变一次
const promise = new Promise((resolve, reject) => {
  resolve("success1");
  reject("error");
  resolve("success2");
});

promise
  .then((res) => {
    console.log("then: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  });
// then: success1
// Promise {<fulfilled>: "success1"}
// 可以一直 then catch
Promise.resolve(1)
  .then((res) => {
    console.log(res);
    return 2;
  })
  .catch((err) => {
    return 3;
  })
  .then((res) => {
    console.log(res);
  });
// 1, 2
Promise.resolve()
  .then(() => {
    return new Error("error!!!");
  })
  .then((res) => {
    console.log("then: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  });
// then:  Error: error!!!
Promise.resolve()
  .then(() => {
    throw new Error("error!!!");
  })
  .then((res) => {
    console.log("then: ", res);
  })
  .catch((err) => {
    console.log("catch: ", err);
  });
// catch:  Error: error!!!
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("once");
    resolve("success");
  }, 1000);
});

const start = Date.now();
// 分别加上 then, 约等于注册了两个事件,会各自获取状态。 链式调用则会包裹一层 Promise.resolve
promise.then((res) => {
  console.log(res, Date.now() - start);
});
promise.then((res) => {
  console.log(res, Date.now() - start);
});
// once
// success 1004
// success 1005
const promise = Promise.resolve().then(() => {
  return promise;
});
promise.catch(console.error);
// 会报错
// TypeError: Chaining cycle detected for promise
//  then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
Promise.resolve(1)
  .then((res) => {
    console.log(res); // => 1
    return 2; // 包装成 Promise.resolve(2)
  })
  .then((res) => {
    console.log(res); // => 2
  });
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);
// 1
// then 需要传入一个合法的回调,否则不执行
Promise.resolve()
  .then(
    function success(res) {
      throw new Error("error");
    },
    function fail1(e) {
      console.error("fail1: ", e);
    }
  )
  .catch(function fail2(e) {
    console.error("fail2: ", e);
  });
// fail2:  Error: error
// process.nextTick 微任务,但是会在下一个循环之前执行,优先执行。
process.nextTick(() => {
  console.log("nextTick");
});
Promise.resolve().then(() => {
  console.log("then");
});
// 宏任务,感觉类似 setTimeout
setImmediate(() => {
  console.log("setImmediate");
});
console.log("end");
// end
// nextTick
// then
// setImmediate
console.log(1);
setTimeout(function () {
  console.log(2);
  let promise = new Promise(function (resolve, reject) {
    console.log(7);
    resolve();
  }).then(function () {
    console.log(8);
  });
}, 1000);
setTimeout(function () {
  console.log(10);
  let promise = new Promise(function (resolve, reject) {
    console.log(11);
    resolve();
  }).then(function () {
    console.log(12);
  });
}, 0);
let promise = new Promise(function (resolve, reject) {
  console.log(3);
  resolve();
})
  .then(function () {
    console.log(4);
  })
  .then(function () {
    console.log(9);
  });
console.log(5);
/// 1,3,5,4,9,10,11,12,2,7,8

async & await

try {
  function test(id) {
    return new Promise(async (resolve, reject) => {});
  }
  // 需要注意的是一个 function 只是 return 一个 promise 但是没有 resolve 或者 rejected 的话,await 是没有结果的,也不会进行赋值。
  const a = await test();
  console.log(a);
} catch (error) {
  console.error("error", error);
}
// 此处 a 不会被打印
function wait() {
  return new Promise((resolve) => setTimeout(resolve, 10 * 100));
}
async function main() {
  console.time();
  const x = wait();
  const y = wait();
  const z = wait();
  await x;
  await y;
  await z;
  console.timeEnd();
}
main();
// 1s 左右
// 因为异步已经在执行了
function wait() {
  return new Promise((resolve) => setTimeout(resolve, 10 * 100));
}
async function main() {
  console.time();
  await wait();
  await wait();
  await wait();
  console.timeEnd();
}
main();
// 3s 左右

promise 和 setTimeout 都会将事件放入异步队列,但 setTimeout 即便是写 0,也会有 4ms 的延迟

console.log("begin");
setTimeout(() => {
  console.log("setTimeout 1");
  Promise.resolve()
    .then(() => {
      console.log("promise 1");
      setTimeout(() => {
        console.log("setTimeout2");
      });
      // 同步 Promise.resolve
    })
    .then(() => {
      console.log("promise 2");
    });

  new Promise((resolve) => {
    console.log("a");
    resolve();
  }).then(() => {
    console.log("b");
  });
}, 0);
console.log("end");

// begin
// end
// setTimeout 1
// a
// promise 1
// b
// promise 2
// setTimeout2

因为 Promise 属于微任务,setTimeout 属于宏任务。

// 两个函数都会一直执行,卡死为止。
function a() {
  console.log("a");
  setTimeout(a);
}

a();

function b() {
  console.log("b");
  Promise.resolve().then(b);
}

b();
setTimeout(function () {
  console.log("setTimeout1"); //8
  new Promise(function (resolve) {
    console.log("promise0"); //9
    resolve();
  }).then(function () {
    console.log("settimeout promise resolveed"); //10
  });
});
setTimeout(function () {
  console.log("setTimeout2"); //11
});
const P = new Promise(function (resolve) {
  console.log("promise"); //1
  for (var i = 0; i < 10000; i++) {
    if (i === 10) {
      console.log("for"); //2
    }
    if (i === 9999) {
      resolve("resolve");
    }
  }
})
  .then(function (val) {
    console.log("resolve1"); //5
  })
  .then(function (val) {
    console.log("resolve2"); //7
  });
new Promise(function (resolve) {
  console.log("promise2"); //3
  resolve("resolve");
}).then(function (val) {
  console.log("resolve3"); //6
});
console.log("console"); //4
Last Updated:
Contributors: yiliang114